5.05. LINQ
LINQ
LINQ (Language Integrated Query)
LINQ — это встроенная в язык C# технология для выполнения запросов к коллекциям данных. Она предоставляет единый и декларативный способ описания операций над последовательностями объектов, независимо от их источника: массивы, списки, базы данных, XML-документы или пользовательские структуры данных. LINQ позволяет писать код, который читается как описание того, что нужно получить, а не как пошаговая инструкция, как это сделать.
Основная цель LINQ — унификация доступа к данным. До появления LINQ разработчику приходилось использовать разные подходы и синтаксисы для работы с разными типами источников: SQL-запросы для баз данных, циклы и условия для коллекций в памяти, XPath и XSLT для XML. LINQ устраняет эту фрагментацию, предоставляя общий язык запросов, понятный на уровне исходного кода и поддерживаемый компилятором.
Два стиля записи запросов
В LINQ существует два эквивалентных способа формулировки запросов: синтаксис запросов (Query Syntax) и синтаксис методов (Method Syntax). Оба стиля транслируются компилятором в один и тот же промежуточный код и дают идентичный результат. Выбор между ними — вопрос предпочтений и читаемости в конкретном контексте.
Синтаксис запросов
Синтаксис запросов напоминает SQL. Он использует ключевые слова, такие как from, where, select, orderby, group, join. Этот стиль особенно удобен при сложных запросах с несколькими источниками данных, соединениями или группировками. Пример:
var result = from student in students
where student.Age > 18
orderby student.Name
select student;
Этот код читается почти как естественный язык: «из коллекции студентов взять тех, чей возраст больше восемнадцати, упорядочить по имени и вернуть их». Компилятор преобразует такой запрос в цепочку вызовов методов расширения, что делает его полностью совместимым с синтаксисом методов.
Синтаксис методов
Синтаксис методов основан на использовании методов расширения, определённых в классе System.Linq.Enumerable (для IEnumerable<T>) и System.Linq.Queryable (для IQueryable<T>). Эти методы принимают делегаты или лямбда-выражения, описывающие логику фильтрации, проекции или сортировки. Пример:
var result = students
.Where(s => s.Age > 18)
.OrderBy(s => s.Name);
Этот стиль ближе к функциональному программированию. Он особенно удобен при простых запросах, когда требуется применить одну или две операции. Многие разработчики предпочитают его за лаконичность и прямую связь с API .NET.
Оба стиля могут сочетаться в одном выражении. Например, можно начать с синтаксиса запросов, а затем продолжить цепочкой методов.
Основные операции LINQ
LINQ предоставляет богатый набор стандартных операторов запросов, которые позволяют выполнять все типичные действия с данными. Эти операторы реализованы как методы расширения и работают с любыми типами, реализующими интерфейс IEnumerable<T> или IQueryable<T>.
Фильтрация
Фильтрация — это выбор элементов, удовлетворяющих заданному условию. Основной оператор — Where. Он принимает предикат (функцию, возвращающую bool) и возвращает новую последовательность, содержащую только те элементы, для которых предикат вернул true.
Пример:
var adults = people.Where(p => p.Age >= 18);
Проекция
Проекция — это преобразование каждого элемента исходной последовательности в новый вид. Оператор Select позволяет создавать новые объекты, извлекать отдельные свойства или комбинировать данные.
Пример:
var names = students.Select(s => s.Name);
var nameAgePairs = students.Select(s => new { s.Name, s.Age });
Проекция может быть скалярной (возврат одного значения) или составной (возврат анонимного типа или нового объекта).
Упорядочение
Упорядочение обеспечивает сортировку элементов по одному или нескольким критериям. Основные методы — OrderBy, OrderByDescending, ThenBy, ThenByDescending. Они поддерживают цепочку сортировок: сначала по основному полю, затем по второстепенному.
Пример:
var sorted = products
.OrderBy(p => p.Category)
.ThenBy(p => p.Price);
Сортировка стабильна: порядок равных элементов сохраняется.
Агрегация
Агрегирующие операторы сводят всю последовательность к одному значению. К ним относятся:
Count— количество элементов;Sum— сумма числовых значений;Average— среднее арифметическое;Min,Max— минимальное и максимальное значение.
Эти методы выполняются немедленно и возвращают скалярный результат, а не последовательность.
Пример:
int totalStudents = students.Count();
double avgGrade = grades.Average();
decimal totalRevenue = orders.Sum(o => o.Amount);
Квантификаторы
Квантификаторы проверяют наличие или универсальность условия в последовательности:
Any— возвращаетtrue, если хотя бы один элемент удовлетворяет условию;All— возвращаетtrue, если все элементы удовлетворяют условию;Contains— проверяет, присутствует ли заданный элемент в последовательности.
Эти методы часто используются в условиях валидации или принятия решений.
Пример:
bool hasMinors = students.Any(s => s.Age < 18);
bool allPassed = exams.All(e => e.Score >= 50);
bool knownUser = validLogins.Contains(inputLogin);
Отложенное выполнение (Deferred Execution)
Одна из ключевых особенностей LINQ — отложенное выполнение. Запрос не выполняется в момент его определения, а только тогда, когда результаты действительно потребуются. Это означает, что переменная, содержащая LINQ-выражение, хранит не данные, а инструкцию по их получению.
Пример:
var query = students.Where(s => s.Grade > 80); // запрос не выполнен
// ... время проходит, коллекция students может измениться ...
foreach (var student in query) // здесь происходит выполнение
{
Console.WriteLine(student.Name);
}
Благодаря отложенному выполнению, LINQ-запросы могут быть динамическими: они всегда работают с актуальным состоянием источника данных. Однако это также требует осторожности: если источник изменяется между определением запроса и его перечислением, результат может отличаться от ожидаемого.
Некоторые операторы, такие как Count, ToList, ToArray, First, Single, принудительно выполняют запрос. Они называются немедленными (eager), потому что возвращают конкретное значение, а не отложенную последовательность.
IEnumerable<T> и IQueryable<T>
LINQ работает с двумя основными интерфейсами: IEnumerable<T> и IQueryable<T>. Хотя они внешне похожи, их поведение принципиально различается.
IEnumerable<T>
Этот интерфейс представляет последовательность объектов в памяти. Все операторы LINQ, применённые к IEnumerable<T>, выполняются на стороне клиента — то есть в самой программе на C#. Данные уже загружены, и запросы работают с ними как с обычными коллекциями.
Пример: работа с List<T>, массивами, результатами File.ReadAllLines().
IQueryable<T>
Этот интерфейс предназначен для работы с удалёнными источниками данных, такими как реляционные базы данных. Он содержит не только последовательность, но и дерево выражения (Expression<Func<...>>), описывающее логику запроса. Когда запрос выполняется, провайдер LINQ (например, Entity Framework Core) анализирует это дерево и транслирует его в язык целевой системы — чаще всего в SQL.
Пример:
var query = dbContext.Students
.Where(s => s.Age > 18)
.Select(s => s.Name);
Здесь dbContext.Students имеет тип IQueryable<Student>. При перечислении query EF Core сгенерирует SQL-запрос вида:
SELECT Name FROM Students WHERE Age > 18
и выполнит его на сервере базы данных. Только имена студентов будут переданы клиенту, а не вся таблица.
Это даёт огромное преимущество в производительности: фильтрация, сортировка и проекция происходят на стороне базы данных, минимизируя объём передаваемых данных.
Различия в поведении
- Для
IEnumerable<T>лямбда-выражения компилируются в делегаты (Func<T, bool>). - Для
IQueryable<T>лямбда-выражения сохраняются как деревья выражений (Expression<Func<T, bool>>), чтобы их можно было проанализировать и преобразовать. - Некоторые операции, допустимые в памяти (например, вызов произвольного метода C# внутри
Where), не могут быть переведены в SQL и вызовут ошибку при работе сIQueryable<T>.
Entity Framework Core активно использует IQueryable<T>, чтобы обеспечить эффективное взаимодействие с базой данных. Понимание разницы между IEnumerable<T> и IQueryable<T> критически важно для написания производительных приложений.
Группировка и соединения
LINQ поддерживает сложные операции, такие как группировка и соединение нескольких последовательностей, что делает его мощным инструментом для анализа связанных данных.
Группировка
Оператор GroupBy позволяет разбить последовательность на подгруппы по заданному ключу. Каждая группа представляет собой объект, содержащий общий ключ и коллекцию элементов, соответствующих этому ключу.
Пример:
var studentsByGrade = students.GroupBy(s => s.GradeLevel);
Результат — последовательность групп, где каждая группа содержит всех студентов одного уровня обучения. Можно дополнительно применять агрегацию внутри групп:
var avgScoreBySubject = exams
.GroupBy(e => e.Subject)
.Select(g => new { Subject = g.Key, Average = g.Average(e => e.Score) });
Этот запрос вычисляет средний балл по каждому предмету. Группировка особенно полезна при подготовке отчётов, статистики или сводных таблиц.
Соединения
LINQ поддерживает несколько типов соединений, аналогичных SQL:
- Inner Join (
Join) — возвращает пары элементов, для которых найдено соответствие в обеих последовательностях. - Group Join — объединяет каждый элемент первой последовательности с коллекцией соответствующих элементов из второй.
- Left Outer Join — реализуется через
GroupJoinиDefaultIfEmpty, чтобы сохранить элементы из левой последовательности даже при отсутствии совпадений.
Пример внутреннего соединения:
var ordersWithCustomers = from order in orders
join customer in customers on order.CustomerId equals customer.Id
select new { order.Id, customer.Name, order.Total };
Соединения позволяют эффективно работать с нормализованными данными, например, связывать заказы с клиентами, товары с категориями или сотрудников с отделами.
Обработка ошибок и пустых результатов
LINQ предоставляет методы, которые помогают безопасно работать с потенциально пустыми последовательностями:
FirstOrDefault()— возвращает первый элемент или значение по умолчанию (nullдля ссылочных типов,0для чисел), если последовательность пуста.SingleOrDefault()— возвращает единственный элемент, если он существует и уникален; иначе выбрасывает исключение или возвращает значение по умолчанию при использованииSingleOrDefault().DefaultIfEmpty()— заменяет пустую последовательность одним элементом по умолчанию.
Эти методы предотвращают исключения InvalidOperationException, которые могут возникнуть при вызове First() или Single() на пустой коллекции.
Пример:
var topStudent = students
.Where(s => s.Grade > 95)
.OrderByDescending(s => s.Grade)
.FirstOrDefault();
if (topStudent != null)
{
Console.WriteLine($"Лучший студент: {topStudent.Name}");
}
Производительность и оптимизация
Хотя LINQ упрощает написание кода, важно понимать его влияние на производительность:
- Каждый оператор LINQ создаёт промежуточный объект (например,
WhereEnumerableIterator), что может привести к накладным расходам при длинных цепочках. - Отложенное выполнение означает, что запрос может выполняться многократно, если к нему обращаются несколько раз. Чтобы избежать этого, следует материализовать результат с помощью
ToList()илиToArray(), если он используется повторно. - При работе с базами данных через Entity Framework Core важно следить за тем, чтобы запросы транслировались в эффективный SQL. Избегайте вызовов методов C# внутри
WhereилиSelect, которые не могут быть переведены в SQL — это приведёт к загрузке всех данных в память и фильтрации на стороне клиента.
Пример плохой практики:
// ❌ Может привести к загрузке всей таблицы в память
var result = dbContext.Products
.Where(p => IsSpecialCategory(p.Category)) // IsSpecialCategory — метод C#
.ToList();
Правильный подход — использовать только выражения, поддерживаемые провайдером:
// ✅ Переводится в SQL
var result = dbContext.Products
.Where(p => p.Category == "Electronics" || p.Category == "Books")
.ToList();
Расширяемость LINQ
LINQ можно расширять, создавая собственные методы расширения. Это позволяет инкапсулировать часто используемую логику в читаемые и переиспользуемые компоненты.
Пример:
public static class LinqExtensions
{
public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int count)
{
var buffer = new Queue<T>();
foreach (var item in source)
{
buffer.Enqueue(item);
if (buffer.Count > count)
yield return buffer.Dequeue();
}
}
}
// Использование
var allButLastThree = numbers.SkipLast(3);
Такие расширения делают код более выразительным и близким к предметной области.
LINQ в реальных сценариях
LINQ активно применяется в повседневной разработке:
- Фильтрация пользовательских данных в веб-приложениях (например, поиск по каталогу товаров).
- Преобразование API-ответов из JSON в удобные DTO-объекты.
- Анализ логов — группировка событий по времени, уровню серьёзности или источнику.
- Подготовка данных для отчётов — агрегация, сортировка, проекция.
- Валидация коллекций — проверка условий с помощью
All,Any.
Благодаря своей декларативной природе, LINQ делает код более читаемым и менее подверженным ошибкам, связанным с ручным управлением циклами и условиями.
Внутреннее устройство: как работает LINQ
Чтобы глубже понять поведение LINQ, полезно заглянуть под капот. Основа LINQ — это интерфейсы IEnumerable<T> и IQueryable<T>, а также механизм итераторов и деревьев выражений.
Итераторы и yield return
Методы расширения в System.Linq.Enumerable реализованы с использованием ключевого слова yield return. Это позволяет создавать последовательности «по требованию», без предварительного выделения памяти под весь результат. Например, метод Where не создаёт новый список, а возвращает объект, который при перечислении проходит по исходной коллекции и возвращает элементы, удовлетворяющие условию.
Такой подход обеспечивает:
- Экономию памяти — данные не копируются до тех пор, пока это не требуется.
- Ленивую обработку — элементы обрабатываются только тогда, когда к ним обращаются.
- Композицию — цепочка операторов (
Where().Select().OrderBy()) строится как последовательность итераторов, каждый из которых оборачивает предыдущий.
Это и есть суть отложенного выполнения: сам запрос — это конвейер обработки, а не готовый результат.
Деревья выражений
При работе с IQueryable<T> лямбда-выражения не компилируются в исполняемый код, а сохраняются в виде деревьев выражений — структуры данных, описывающей операции, переменные и вызовы. Провайдер LINQ (например, Entity Framework Core) анализирует это дерево и преобразует его в команду на другом языке — чаще всего SQL.
Пример дерева:
Expression<Func<Student, bool>> expr = s => s.Age > 18;
Здесь expr содержит не функцию, а описание: «сравнить свойство Age объекта типа Student с числом 18». Это описание можно прочитать, модифицировать или перевести.
Благодаря деревьям выражений становится возможным:
- Построение динамических запросов.
- Создание универсальных фильтров и поисковых систем.
- Интеграция с внешними системами, которые не понимают C#, но понимают SQL, XPath или другие языки запросов.
Сравнение с традиционными циклами
Раньше, до LINQ, для фильтрации списка студентов старше 18 лет пришлось бы писать:
var adults = new List<Student>();
foreach (var student in students)
{
if (student.Age > 18)
{
adults.Add(student);
}
}
LINQ заменяет этот шаблон одной строкой:
var adults = students.Where(s => s.Age > 18);
Преимущества:
- Краткость и выразительность.
- Отсутствие ручного управления коллекцией.
- Возможность легко комбинировать операции.
- Уменьшение количества ошибок (например, забыть создать список или добавить элемент).
LINQ не заменяет циклы полностью — в случаях, где требуется сложная логика с побочными эффектами, foreach остаётся уместным. Но для чистых операций над данными LINQ — предпочтительный выбор.
Поддержка в других языках и платформах
Хотя LINQ родился в экосистеме .NET, его идеи повлияли на другие языки:
- Java имеет Stream API, во многом вдохновлённый LINQ.
- JavaScript использует методы вроде
filter,map,reduce— аналогиWhere,Select, агрегации. - Python предлагает генераторы списков и функции
filter,map.
Однако ни один из этих аналогов не достигает уровня интеграции LINQ с языком и компилятором. Только в C# запросы являются частью синтаксиса, проверяются на этапе компиляции и поддерживают два равноценных стиля записи.
Типичные ошибки и как их избежать
-
Многократное выполнение отложенного запроса
Если использовать один и тот же LINQ-запрос несколько раз без материализации, он будет выполняться заново каждый раз. Это может привести к несогласованности данных или снижению производительности.
Решение: вызватьToList()илиToArray(), если результат используется многократно. -
Смешивание
IEnumerable<T>иIQueryable<T>
ПреобразованиеIQueryable<T>вIEnumerable<T>(например, через.AsEnumerable()) заставляет последующие операции выполняться в памяти, а не в базе данных. Это может привести к загрузке лишних данных.
Решение: выполнять фильтрацию и проекцию на стороне базы данных, пока это возможно. -
Использование не транслируемых методов в EF Core
Вызов произвольного метода C# внутриWhereпри работе сIQueryable<T>вызовет исключение или неэффективную загрузку всех данных.
Решение: ограничиваться выражениями, поддерживаемыми провайдером. -
Игнорирование null-значений
При работе с проекциями или соединениями важно учитывать, что некоторые свойства могут бытьnull.
Решение: использовать безопасную навигацию (?.) или проверки перед доступом.
Продвинутые сценарии
Динамические запросы
Иногда условия фильтрации неизвестны на этапе компиляции — например, пользователь выбирает критерии в интерфейсе. В таких случаях можно строить выражения программно с помощью класса Expression.
Пример:
var param = Expression.Parameter(typeof(Student), "s");
var body = Expression.GreaterThan(
Expression.Property(param, "Age"),
Expression.Constant(18)
);
var lambda = Expression.Lambda<Func<Student, bool>>(body, param);
var query = students.Where(lambda);
Это позволяет создавать гибкие системы фильтрации без жёсткой привязки к конкретным условиям.
Параллельные запросы (PLINQ)
Для обработки больших объёмов данных в памяти можно использовать Parallel LINQ (PLINQ). Он автоматически распределяет работу между потоками:
var result = data.AsParallel()
.Where(x => x.IsValid)
.Select(x => Process(x))
.ToArray();
PLINQ полезен при CPU-интенсивных операциях, но требует осторожности: не все операции потокобезопасны, а накладные расходы на параллелизм могут перевесить выгоду при малых данных.
Классы, методы и свойства
Основной класс: System.Linq.Enumerable
Большинство методов LINQ реализованы как статические методы расширения в классе System.Linq.Enumerable. Этот класс предоставляет функциональность для работы с любыми типами, реализующими интерфейс IEnumerable<T>. Ниже перечислены все основные группы методов с пояснением их назначения, сигнатур и особенностей использования.
1. Фильтрация
-
Where<TSource>(IEnumerable<TSource>, Func<TSource, bool>)
Возвращает элементы последовательности, для которых заданный предикат возвращаетtrue. -
Where<TSource>(IEnumerable<TSource>, Func<TSource, int, bool>)
То же самое, но предикат получает также индекс элемента (0-based). Полезно при фильтрации по позиции.
2. Проекция
-
Select<TSource, TResult>(IEnumerable<TSource>, Func<TSource, TResult>)
Преобразует каждый элемент последовательности с помощью указанной функции проекции. -
Select<TSource, TResult>(IEnumerable<TSource>, Func<TSource, int, TResult>)
Проекция с доступом к индексу элемента. -
SelectMany<TSource, TResult>(IEnumerable<TSource>, Func<TSource, IEnumerable<TResult>>)
Проецирует каждый элемент в коллекцию, а затем «выравнивает» результат в одну последовательность. АналогflatMapв других языках. -
SelectMany<TSource, TCollection, TResult>(...)
Расширенная версия с возможностью комбинировать исходный элемент и элементы его коллекции в новый результат.
3. Упорядочение
-
OrderBy<TSource, TKey>(IEnumerable<TSource>, Func<TSource, TKey>)
Сортирует элементы по возрастанию ключа. -
OrderByDescending<TSource, TKey>(...)
Сортировка по убыванию. -
ThenBy<TSource, TKey>(IOrderedEnumerable<TSource>, Func<TSource, TKey>)
Дополнительная сортировка послеOrderBy. Работает только с уже упорядоченной последовательностью (IOrderedEnumerable<T>). -
ThenByDescending<TSource, TKey>(...)
Дополнительная сортировка по убыванию. -
Reverse<TSource>(IEnumerable<TSource>)
Инвертирует порядок элементов.
4. Группировка
-
GroupBy<TSource, TKey>(IEnumerable<TSource>, Func<TSource, TKey>)
Группирует элементы по ключу. ВозвращаетIEnumerable<IGrouping<TKey, TSource>>. -
GroupBy<TSource, TKey, TElement>(..., Func<TSource, TElement>)
Позволяет преобразовать элементы внутри каждой группы. -
GroupBy<TSource, TKey, TResult>(..., Func<TKey, IEnumerable<TSource>, TResult>)
Позволяет сразу создать результирующий объект из ключа и группы.
5. Соединения
-
Join<TOuter, TInner, TKey, TResult>(IEnumerable<TOuter>, IEnumerable<TInner>, Func<TOuter, TKey>, Func<TInner, TKey>, Func<TOuter, TInner, TResult>)
Выполняет внутреннее соединение двух последовательностей по ключу. -
GroupJoin<TOuter, TInner, TKey, TResult>(...)
Групповое соединение: каждый элемент внешней последовательности объединяется с коллекцией соответствующих элементов внутренней.
6. Агрегация
-
Aggregate<TSource>(IEnumerable<TSource>, Func<TSource, TSource, TSource>)
Последовательно применяет функцию аккумулятора к элементам. Например, можно вычислить произведение чисел. -
Aggregate<TSource, TAccumulate>(..., TAccumulate seed, Func<TAccumulate, TSource, TAccumulate>)
Агрегация с начальным значением. -
Aggregate<TSource, TAccumulate, TResult>(..., TResult resultSelector)
Агрегация с финальным преобразованием результата. -
Count<TSource>(IEnumerable<TSource>)
Возвращает количество элементов. -
Count<TSource>(IEnumerable<TSource>, Func<TSource, bool>)
Подсчитывает элементы, удовлетворяющие условию. -
LongCount<TSource>(...)
То же, чтоCount, но возвращаетlong— для очень больших коллекций. -
Sum(IEnumerable<int>),Average(IEnumerable<double>),Min,Max
Существуют перегрузки для всех числовых типов (int,long,float,double,decimal) и для последовательностей объектов с проекцией:
Sum<TSource>(IEnumerable<TSource>, Func<TSource, int>).
7. Квантификаторы
-
Any<TSource>(IEnumerable<TSource>)
Возвращаетtrue, если последовательность содержит хотя бы один элемент. -
Any<TSource>(IEnumerable<TSource>, Func<TSource, bool>)
Проверяет наличие элемента, удовлетворяющего условию. -
All<TSource>(IEnumerable<TSource>, Func<TSource, bool>)
Возвращаетtrue, если все элементы удовлетворяют условию. -
Contains<TSource>(IEnumerable<TSource>, TSource)
Проверяет наличие значения в последовательности. -
Contains<TSource>(IEnumerable<TSource>, TSource, IEqualityComparer<TSource>)
То же, с пользовательским компаратором.
8. Элементы
-
First<TSource>(IEnumerable<TSource>)
Возвращает первый элемент. Выбрасывает исключение, если последовательность пуста. -
FirstOrDefault<TSource>(...)
Возвращает первый элемент или значение по умолчанию. -
Last<TSource>(...),LastOrDefault<TSource>(...)
Аналогично, но для последнего элемента. -
Single<TSource>(...),SingleOrDefault<TSource>(...)
Ожидают ровно один элемент (или ноль дляOrDefault). Используются для проверки уникальности. -
ElementAt<TSource>(IEnumerable<TSource>, int index)
Возвращает элемент по индексу. Выбрасывает исключение при выходе за границы. -
ElementAtOrDefault<TSource>(...)
Возвращает элемент или значение по умолчанию.
9. Разбиение и пропуск
-
Take<TSource>(IEnumerable<TSource>, int count)
Возвращает первыеcountэлементов. -
TakeLast<TSource>(IEnumerable<TSource>, int count)
Возвращает последниеcountэлементов (.NET Core 3.0+). -
Skip<TSource>(IEnumerable<TSource>, int count)
Пропускает первыеcountэлементов. -
SkipLast<TSource>(IEnumerable<TSource>, int count)
Пропускает последниеcountэлементов. -
TakeWhile<TSource>(IEnumerable<TSource>, Func<TSource, bool>)
Берёт элементы, пока предикат возвращаетtrue. Останавливается при первомfalse. -
SkipWhile<TSource>(...)
Пропускает элементы, пока предикат возвращаетtrue, затем возвращает остальные.
10. Объединение и разность
-
Concat<TSource>(IEnumerable<TSource>, IEnumerable<TSource>)
Объединяет две последовательности (включая дубликаты). -
Union<TSource>(IEnumerable<TSource>, IEnumerable<TSource>)
Объединение без дубликатов. -
Intersect<TSource>(...)
Возвращает общие элементы. -
Except<TSource>(...)
Возвращает элементы первой последовательности, отсутствующие во второй.
Все эти методы имеют перегрузки с IEqualityComparer<TSource> для настройки сравнения.
11. Преобразование и материализация
-
ToArray<TSource>(IEnumerable<TSource>)
Преобразует в массив. -
ToList<TSource>(...)
Преобразует вList<T>. -
ToDictionary<TSource, TKey>(..., Func<TSource, TKey>)
Создаёт словарь по ключу. -
ToDictionary<TSource, TKey, TElement>(..., Func<TSource, TElement>)
Словарь с преобразованием значений. -
ToLookup<TSource, TKey>(...)
Создаёт неизменяемую группировку (ILookup<TKey, TSource>), аналогичнуюGroupBy, но материализованную. -
Cast<TResult>(IEnumerable)
Преобразует необобщённую последовательность вIEnumerable<TResult>. -
OfType<TResult>(IEnumerable)
Фильтрует элементы, совместимые с типомTResult, и приводит их.
12. Сравнение
-
SequenceEqual<TSource>(IEnumerable<TSource>, IEnumerable<TSource>)
Проверяет, равны ли две последовательности поэлементно. -
SequenceEqual<TSource>(..., IEqualityComparer<TSource>)
С пользовательским компаратором.
13. Другие утилиты
-
DefaultIfEmpty<TSource>(IEnumerable<TSource>)
Возвращает последовательность с одним элементом по умолчанию, если исходная пуста. -
DefaultIfEmpty<TSource>(..., TSource defaultValue)
С указанием значения по умолчанию. -
Zip<TFirst, TSecond, TResult>(IEnumerable<TFirst>, IEnumerable<TSecond>, Func<TFirst, TSecond, TResult>)
Объединяет две последовательности попарно. Результат ограничен длиной более короткой последовательности. -
Distinct<TSource>(IEnumerable<TSource>)
Удаляет дубликаты. -
DistinctBy<TSource, TKey>(IEnumerable<TSource>, Func<TSource, TKey>)
Удаляет дубликаты по ключу (.NET 6+).
Интерфейсы LINQ
-
IEnumerable<T>
Основной интерфейс для перечисления элементов. Реализуется всеми коллекциями в .NET. -
IOrderedEnumerable<T>
РасширениеIEnumerable<T>, возвращаемоеOrderBy. Позволяет вызыватьThenBy. -
IGrouping<TKey, TElement>
Представляет одну группу послеGroupBy. Имеет свойствоKeyи является перечислимой коллекцией элементов. -
ILookup<TKey, TElement>
Неизменяемый аналогDictionary<TKey, IEnumerable<TElement>>, возвращаемыйToLookup.
Класс System.Linq.Queryable
Этот класс предоставляет те же самые операторы, что и Enumerable, но для типа IQueryable<T>. Отличие — все лямбда-выражения принимаются как Expression<Func<...>>, а не как Func<...>. Это позволяет провайдерам (например, Entity Framework) анализировать логику запроса и транслировать её.
Пример:
public static IQueryable<TSource> Where<TSource>(
this IQueryable<TSource> source,
Expression<Func<TSource, bool>> predicate);
Класс System.Linq.ILookup<TKey, TElement>
Интерфейс, представляющий предварительно сгруппированные данные. В отличие от GroupBy, который отложен, ToLookup выполняется немедленно и позволяет многократно обращаться к группам без повторного перебора.